package com.sangmo.fts.common.authority;

import com.sangmo.boot.framework.common.session.UserSession;
import com.sangmo.boot.framework.common.utils.BeanUtils;
import com.sangmo.boot.framework.data.entity.Entity;
import com.sangmo.boot.framework.data.filter.Criteria;
import com.sangmo.fts.common.authority.sql.INativeSupport;
import com.sangmo.fts.common.authority.sql.NativeSearchContext;
import com.sangmo.fts.common.authority.sql.WhereModifier;
import org.apache.commons.lang3.StringUtils;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class AuthorityFilterChain {
    private static AuthorityFilterChain INSTANCE = new AuthorityFilterChain();


    private Map<String, IAuthorityFilter> ROW_FILTERS = new HashMap<>();
    private Map<String, Class> TYPE_CLASS = new HashMap<>();
    private Map<String, Class> AUTHORITY_ENTITY_MAP = new HashMap<>();

    public static AuthorityFilterChain getInstance() {
        return INSTANCE;
    }

    public void register(IAuthorityFilter filter) {
        ROW_FILTERS.put(filter.getAnnotation().getName(), filter);
    }

    private static ThreadLocal<Boolean> AUTHORITY_CHECK = new ThreadLocal<>();

    public boolean isAuthorityChecked() {
        return Boolean.TRUE.equals(AUTHORITY_CHECK.get());
    }

    public void setAuthorityChecked() {
        AUTHORITY_CHECK.set(true);
    }

    public void clear() {
        AUTHORITY_CHECK.remove();
    }

    public boolean shouldSkipAuthorityCheck() {
        return this.isAuthorityChecked();
    }

    public boolean hasAllRights() {
        return null != UserSession.findPermission("*:*");
    }

    public void registerEntity(Class entityClass, Class authorityClass) {
        AUTHORITY_ENTITY_MAP.put(entityClass.getSimpleName(), authorityClass);
        TYPE_CLASS.put(entityClass.getSimpleName(), entityClass);
    }

    public Class findAuthorityClass(Class entityClass) {
        return AUTHORITY_ENTITY_MAP.get(entityClass.getSimpleName());
    }


    private INativeSupport.NativeSearchFilterResult deduceNative(NativeSearchContext context, int expectedRight, Annotation ann, Map<String, Annotation> annotationMap) {
        IAuthorityFilter filter = ROW_FILTERS.get(ann.annotationType().getName());
        INativeSupport.NativeSearchFilterResult filterResult = filter.doSearchFilter(context, ann, expectedRight);

        if (filterResult.isRejected()) {
            return filterResult;
        } else {
            String dependsOn = (String)BeanUtils.findAnnotationProperty(ann, "dependsOn", null);
            if (StringUtils.isNotEmpty(dependsOn)) {
                String[] dps = dependsOn.split(",", -1);
                for (String dp : dps) {
                    Annotation dependsOnAnn = annotationMap.get(dp);
                    if (null == dependsOnAnn) {
                        throw new RuntimeException("Authority dependency is missing: " + dp);
                    }
                    int dependsOnRight = (int) BeanUtils.findAnnotationProperty(ann, "anyRights", null);

                    INativeSupport.NativeSearchFilterResult dependsOnResult = this.deduceNative(context, dependsOnRight, dependsOnAnn, annotationMap);
                    if (dependsOnResult.isRejected()) {
                        return dependsOnResult;
                    } else {
                        if (StringUtils.isNotEmpty(dependsOnResult.getSql())) {
                            filterResult.setSql(String.format(" (%s) AND (%s)", filterResult.getSql(), dependsOnResult.getSql()));
                            filterResult.getVariables().putAll(dependsOnResult.getVariables());
                        }
                    }
                }
            }
        }
        return filterResult;
    }

    private IAuthorityFilter.SearchFilterResult deduce(EntitySearchContext context, int expectedRight, Annotation ann, Map<String, Annotation> annotationMap) {

        IAuthorityFilter filter = ROW_FILTERS.get(ann.annotationType().getName());
        IAuthorityFilter.SearchFilterResult filterResult = filter.doSearchFilter(context, ann, expectedRight);

        if (filterResult.isRejected()) {
            return filterResult;
        } else {
            String dependsOn = (String)BeanUtils.findAnnotationProperty(ann, "dependsOn", null);
            if (StringUtils.isNotEmpty(dependsOn)) {
                String[] dps = dependsOn.split(",", -1);
                for (String dp : dps) {
                    Annotation dependsOnAnn = annotationMap.get(dp);
                    if (null == dependsOnAnn) {
                        throw new RuntimeException("Authority dependency is missing: " + dp);
                    }
                    int dependsOnRight = (int) BeanUtils.findAnnotationProperty(ann, "anyRights", null);
                    IAuthorityFilter.SearchFilterResult dependsOnResult = this.deduce(context, dependsOnRight, dependsOnAnn, annotationMap);
                    if (dependsOnResult.isRejected()) {
                        return dependsOnResult;
                    } else {
                        if (null != dependsOnResult.predicate) {
                            filterResult.setPredicate(Criteria.concat(context.getCriteriaBuilder(), filterResult.predicate, dependsOnResult.predicate, false));
                        }
                    }
                }
            }
            return filterResult;
        }
    }

    private boolean deduce(EntityContext context, int expectedRight, Entity entity, Annotation ann, Map<String, Annotation> annotationMap) {
        boolean isCanAccess = false;

        IAuthorityFilter filter = ROW_FILTERS.get(ann.annotationType().getName());
        if (null != context.getId()) {
            isCanAccess = filter.filter(context, ann, expectedRight);
        } else {
            isCanAccess = filter.filterCreation(entity, ann, expectedRight);
        }
        if (!isCanAccess) {
            return isCanAccess;
        } else {
            String dependsOn = (String)BeanUtils.findAnnotationProperty(ann, "dependsOn", null);
            if (StringUtils.isNotEmpty(dependsOn)) {
                String[] dps = dependsOn.split(",", -1);
                for (String dp : dps) {
                    Annotation dependsOnAnn = annotationMap.get(dp);
                    if (null == dependsOnAnn) {
                        throw new RuntimeException("Authority dependency is missing: " + dp);
                    }
                    int dependsOnRight = (int) BeanUtils.findAnnotationProperty(dependsOnAnn, "anyRights", null);
                    boolean dependsOnResult = this.deduce(context, dependsOnRight, entity, dependsOnAnn, annotationMap);
                    if (!dependsOnResult) {
                        return dependsOnResult;
                    } else {
                        isCanAccess = isCanAccess && dependsOnResult;
                    }
                }
            }
            return isCanAccess;
        }
    }

    private void resolveDependencies(Annotation[] annotations, Map<String, Annotation> annotationMap, List<Annotation> standalones) {
        Set<String> dependencies = new HashSet<>();
        for (Annotation ann : annotations) {
            if (ROW_FILTERS.containsKey(ann.annotationType().getName())) {
                String name = (String)BeanUtils.findAnnotationProperty(ann, "name", null);
                if (StringUtils.isNotEmpty(name)) {
                    annotationMap.put(name, ann);
                }

                String dependsOn = (String)BeanUtils.findAnnotationProperty(ann, "dependsOn", null);
                if (StringUtils.isNotEmpty(dependsOn)) {
                    String[] dps = dependsOn.split(",", -1);
                    for (String dp : dps) {
                        dependencies.add(dp);
                    }
                }
            }
        }

        for (Annotation ann : annotations) {
            if (ROW_FILTERS.containsKey(ann.annotationType().getName())) {
                String name = (String) BeanUtils.findAnnotationProperty(ann, "name", null);

                if (!dependencies.contains(name)) {
                    standalones.add(ann);
                }
            }
        }

    }

    /*
    * 授权过滤器之间可以存在依赖关系，过滤器依赖采用 AND 的方式合并查询结果
    * */
    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
    public Predicate doSearchFilters(EntitySearchContext context, int expectedRight) {
        if (hasAllRights()) {
            return null;
        }

        Class rootClass = this.findAuthorityClass(context.getRoot().getJavaType());
        if (null == rootClass) {
            rootClass = context.getRoot().getClass();
        }


        Annotation[] annotations =  BeanUtils.findAnnotations(rootClass);

        Map<String, Annotation> annotationMap = new HashMap<>();
        List<Annotation> standalones = new ArrayList<>();
        this.resolveDependencies(annotations, annotationMap, standalones);


        Predicate modify = null;

        boolean hasRejected = false;


        for (Annotation ann : standalones) {
            IAuthorityFilter.SearchFilterResult filterResult = this.deduce(context, expectedRight, ann, annotationMap);
            if (filterResult.isRejected()) {
                hasRejected = true;
            } else {
                if (null != filterResult.predicate) {
                    modify = Criteria.concat(context.getCriteriaBuilder(), modify, filterResult.predicate, true);
                }
            }
        }

        if (null == modify && hasRejected) {
            modify = reject(context);
        }

        return modify;
    }

    private Predicate reject(EntitySearchContext context) {
        Path path = context.getRoot().get("id");
        return context.getCriteriaBuilder().equal(path, ""); //no read permission.
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
    public boolean doObjectFilters(EntityContext context, int expectedRight) {
        return this.doInternalObjectFilter(context, expectedRight, null);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
    public boolean doCreateObjectFilters(Entity entity, int expectedRight) {
        return this.doInternalObjectFilter(new EntityContext(entity.getClass(), (Long) entity.getId()), expectedRight, entity);
    }

    private boolean doInternalObjectFilter(EntityContext context, int expectedRight, Entity entity) {
        if (hasAllRights()) {
            return true;
        }

        if (this.shouldSkipAuthorityCheck()) {
            return true;
        }
        boolean isCanAccess = false;

        Class rootClass = this.findAuthorityClass(context.getEntityClass());
        if (null == rootClass) {
            rootClass = context.getEntityClass();
        }

        Annotation[] annotations =  BeanUtils.findAnnotations(rootClass);

        Map<String, Annotation> annotationMap = new HashMap<>();
        List<Annotation> standalones = new ArrayList<>();
        this.resolveDependencies(annotations, annotationMap, standalones);

        for (Annotation ann : standalones) {
            isCanAccess = this.deduce(context, expectedRight, entity, ann, annotationMap);
            if (isCanAccess) {
                break;
            }

        }
        return isCanAccess;
    }

    public boolean isSupported(String type) {
        return null != TYPE_CLASS.get(type);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
    public boolean doTypeAuthorityCheck(String type, Long id, int expectedRight) {
        if (hasAllRights()) {
            return true;
        }

        Class clazz = TYPE_CLASS.get(type);
        return this.doObjectFilters(new EntityContext(clazz, id), expectedRight);
    }

    public static Criteria.CriteriaModifier createModifier(final int right) {
        return new Criteria.CriteriaModifier() {
            @Override
            public Predicate onSearch(Root root, CriteriaBuilder cb, Predicate p) {
                EntitySearchContext context = new EntitySearchContext(root, cb);
                p = AuthorityFilterChain.getInstance().doSearchFilters(context, right);
                return p;
            }
        };
    }

    public WhereModifier createNativeModifier(String alias, Class root, final int right) {
        NativeSearchContext context = new NativeSearchContext(alias, root);
        return this.doNativeSearchFilters(context, right);
    }

    public WhereModifier doNativeSearchFilters(NativeSearchContext context, int expectedRight) {
        if (hasAllRights()) {
            return new WhereModifier();
        }

        Class rootClass = this.findAuthorityClass(context.getRoot());
        if (null == rootClass) {
            rootClass = context.getRoot();
        }

        Annotation[] annotations =  BeanUtils.findAnnotations(rootClass);

        Map<String, Annotation> annotationMap = new HashMap<>();
        List<Annotation> standalones = new ArrayList<>();
        this.resolveDependencies(annotations, annotationMap, standalones);

        boolean hasRejected = false;
        String modify = "";

        Map<String, Object> variables = new HashMap<>();

        for (Annotation ann : standalones) {
            IAuthorityFilter filter = ROW_FILTERS.get(ann.annotationType().getName());

            if (null != filter) {
                INativeSupport.NativeSearchFilterResult filterResult = this.deduceNative(context, expectedRight, ann, annotationMap);

                if (filterResult.isRejected()) {
                    hasRejected = true;
                } else {
                    if (StringUtils.isNotEmpty(filterResult.getSql())) {
                        if (StringUtils.isNotEmpty(modify)) {
                            modify += " OR ";
                        }
                        modify += String.format("(%s) ", filterResult.getSql());
                        variables.putAll(filterResult.getVariables());
                    }
                }
            }
        }

        if (StringUtils.isEmpty(modify) && hasRejected) {
            modify = reject(context);
        }

        return new WhereModifier(modify, variables);
    }

    private String reject(NativeSearchContext context) {
        return " 1 = 0 ";
    }

}
